package v1

import (
	"auth/client/grpc"
	log "auth/collector/logger"
	"auth/config"
	"auth/constants"
	"auth/lib/authentication"
	"auth/model"
	"auth/utils"
	"context"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"gitee.com/ai_camera/proto/device"
	"github.com/gin-gonic/gin"
	"go.uber.org/zap"
	"io"
	"net/http"
	"sort"
	"strconv"
	"strings"
	"time"
)

var whiteUrlMap map[string]struct{}

func InitWhiteUrlMap() {
	whiteUrlMap = make(map[string]struct{})
	for _, v := range config.Conf.WhiteUrl {
		whiteUrlMap[v] = struct{}{}
	}
	return
}

var Auth = &auth{}

func init() {
	RegisterRouter(Auth)
}

type auth struct{}

// Init
func (a *auth) Init(r *gin.RouterGroup) {
	r.GET("/auth", a.auth)
	r.POST("/mqtt_auth", a.authMqtt)
}

func (a *auth) auth(c *gin.Context) {
	if _, ok := whiteUrlMap[c.Request.URL.Path]; ok {
		c.JSON(http.StatusOK, gin.H{})
		return
	}

	c.Header("Content-Type", "application/json")

	os := c.GetHeader(constants.Source)
	ctx := c.Request.Context()

	if os == constants.SourceApp {
		token := c.GetHeader("Authorization")
		userId, err := a.authUser(ctx, token)
		if err != nil {
			c.JSON(http.StatusUnauthorized, gin.H{})
			return
		}
		c.Header(constants.UserId, userId)
		c.JSON(http.StatusOK, gin.H{})
		return
	}

	// 设备端鉴权
	if os == constants.SourceDevice {
		signature := c.GetHeader(constants.Signature)
		timestamp := c.GetHeader(constants.Timestamp)
		deviceId := c.GetHeader(constants.DeviceId)
		nonce := c.GetHeader(constants.Nonce)

		now := time.Now()
		timestampInt, err := strconv.ParseInt(timestamp, 10, 64)
		if err != nil {
			c.JSON(http.StatusUnauthorized, gin.H{})
			return
		}
		if timestampInt > now.Unix() || timestampInt < now.Unix()-300 {
			c.JSON(http.StatusUnauthorized, gin.H{})
			return
		}

		params := make(map[string]interface{})

		// path
		query := c.Request.URL.Query()
		for k, v := range query {
			params[k] = v[0]
		}

		// body default 3MB
		bs, err := io.ReadAll(io.LimitReader(c.Request.Body, int64(3<<20)))
		if err != nil {
			log.ErrorCtx(ctx, "failed to read body", zap.Error(err))
			c.JSON(http.StatusUnauthorized, gin.H{})
			return
		}
		if len(bs) > 0 {
			bodyParams := make(map[string]interface{})
			// 存在body为空的情况
			_ = json.Unmarshal(bs, &bodyParams)
			for k, v := range bodyParams {
				params[k] = v
			}
		}

		params[constants.DeviceId] = deviceId
		params[constants.Source] = os
		params[constants.Nonce] = nonce
		params[constants.Timestamp] = timestamp

		ok, err := a.authDevice(ctx, deviceId, signature, params)
		if err != nil {
			c.JSON(http.StatusUnauthorized, gin.H{})
			return
		}
		if ok {
			c.Header(constants.DeviceId, c.GetHeader(constants.DeviceId))
			c.JSON(http.StatusOK, gin.H{})
			return
		}
		c.JSON(http.StatusUnauthorized, gin.H{})
		return
	}

	log.ErrorCtx(ctx, "invalid source", zap.String("source", os))

	c.JSON(http.StatusUnauthorized, gin.H{})
	return
}

func (a *auth) authUser(ctx context.Context, token string) (string, error) {
	if len(token) == 0 {
		log.ErrorCtx(ctx, "auth user token empty")
		return "", errors.New("token is empty")
	}
	parts := strings.SplitN(token, " ", 2)
	if !(len(parts) == 2 && parts[0] == "Bearer") {
		log.ErrorCtx(ctx, "token is invalid")
		return "", errors.New("token is invalid")
	}

	opts, err := authentication.Extract(parts[1])
	if err != nil {
		log.ErrorCtx(ctx, "extract opts fail", zap.Error(err))
		return "", errors.New("extract opts fail")
	}

	return opts.UserID, nil
}

func (a *auth) authDevice(ctx context.Context, deviceId, sign string, params map[string]interface{}) (bool, error) {
	deviceInfo, err := grpc.DeviceClient.GetDeviceInfo(ctx, &device.DeviceInfoRequest{DeviceId: deviceId})
	if err != nil {
		log.ErrorCtx(ctx, "grpc get device info fail", zap.Error(err))
		return false, err
	}

	s := getSign(deviceInfo.DeviceKey, params)

	return s == sign, nil
}

func (a *auth) authMqtt(c *gin.Context) {
	var request model.AuthMqttRequest
	response := model.AuthMqttResponse{
		Result: "deny",
	}
	ctx := c.Request.Context()
	if err := c.ShouldBindJSON(&request); err != nil {
		log.ErrorCtx(ctx, "bind mqtt fail", zap.Error(err))
		c.JSON(http.StatusOK, response)
		return
	}

	defer func() {
		log.InfoCtx(ctx, "auth mqtt", zap.String("req", utils.ToJsonString(request)), zap.String("resp", utils.ToJsonString(response)))
	}()

	deviceInfo, err := grpc.DeviceClient.GetDeviceInfo(ctx, &device.DeviceInfoRequest{DeviceId: request.Username})
	if err != nil {
		log.ErrorCtx(ctx, "grpc get device info fail", zap.Error(err))
		c.JSON(http.StatusOK, response)
		return
	}

	hash := sha256.New()
	hash.Write([]byte(deviceInfo.DeviceKey))
	ps := hex.EncodeToString(hash.Sum(nil))

	if ps != request.Password {
		log.ErrorCtx(ctx, "mqtt auth fail", zap.String("username", request.Username))
		c.JSON(http.StatusOK, response)
		return
	}

	response.Result = "allow"
	response.Acl = []*model.MqttAcl{
		{
			Permission: "allow",
			Action:     "publish",
			Topic:      fmt.Sprintf("ivisionic/server/%s/result", request.Username),
		},
		{
			Permission: "allow",
			Action:     "subscribe",
			Topic:      fmt.Sprintf("ivisionic/camera/%s/event", request.Username),
		},
		{
			Permission: "allow",
			Action:     "subscribe",
			Topic:      fmt.Sprintf("ivisionic/4G/%s/wake_up", request.Username),
		},
		{
			Permission: "deny",
			Action:     "all",
			Topic:      "#",
		},
	}

	c.JSON(http.StatusOK, response)

}

func getSign(deviceKey string, params map[string]interface{}) string {
	var (
		buf     strings.Builder
		keyList []string
	)
	for k := range params {
		keyList = append(keyList, k)
	}
	sort.Strings(keyList)
	for _, k := range keyList {
		if v, ok := params[k]; ok && v != "" {
			buf.WriteString(k)
			buf.WriteByte('=')
			buf.WriteString(convertToString(v))
			buf.WriteByte('&')
		}
	}
	buf.WriteString("device_key")
	buf.WriteByte('=')
	buf.WriteString(deviceKey)

	hash := hmac.New(sha256.New, []byte(deviceKey))
	hash.Write([]byte(buf.String()))

	return strings.ToUpper(hex.EncodeToString(hash.Sum(nil)))
}

func convertToString(value any) string {
	if value == nil {
		return ""
	}
	v, ok := value.(string)
	if ok {
		return v
	}

	bytes, err := json.Marshal(value)
	if err != nil {
		return ""
	}

	return string(bytes)
}
